package com.util;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import org.apache.commons.codec.binary.Base64;

import com.exception.AuthenticationException;

/**
 * Authenticates the license key installed on the server. The default license
 * key location is <code>user.home/LICENSE_KEY_DIR/PRODUCT_NAME.key</code>.
 * 
 * @author Anthony.M
 *
 */
public class AuthenticationUtils {

	/** Product name. */
	private static final String PRODUCT_NAME = "WebMES";

	/** License key directory. */
	private static final String LICENSE_KEY_DIR = ".iCIM";

	private static final int HEAD_LENGTH = 6;

	/**
	 * Gets the MAC address.
	 * 
	 * @return MAC address encoded by hex string
	 * @throws IOException 
	 */
	public static final String getMacAddress() throws IOException {
		NetworkInterface ni = NetworkInterface.getByInetAddress(InetAddress.getLocalHost());
		byte[] ha = ni.getHardwareAddress();
		if (ha == null && isLinux()) {
		    return getLinuxMac();
		}
		StringBuilder mac = new StringBuilder();
		for (int i = 0; i < ha.length; i++) {
			String hex = Integer.toHexString(ha[i] & 0xFF);
			if (hex.length() < 2) {
				hex = "0" + hex;
			}
			mac.append(hex).append("-");
		}
		return mac.deleteCharAt(mac.length() - 1).toString().toUpperCase();
	}

	/**
	 * Gets the server id by MAC address.
	 * 
	 * @param mac MAC address
	 * @return server id
	 * @throws NoSuchAlgorithmException
	 */
	public static final String getServerId(String mac) throws NoSuchAlgorithmException {
		MessageDigest md = MessageDigest.getInstance("MD5");
		byte[] md5 = md.digest(mac.getBytes());
		StringBuilder serverId = new StringBuilder();
		for (int i = 0; i < md5.length; i++) {
			String hex = Integer.toHexString(md5[i] & 0xFF);
			if (hex.length() < 2) {
				hex = "0" + hex;
			}
			serverId.append(hex);
		}
		return serverId.toString().toUpperCase();
	}

	/**
	 * Loads the license key file.
	 * 
	 * @return key bytes
	 * @throws IOException
	 */
	public static final byte[] loadLicenseKey() throws IOException {
		String userHome = System.getProperty("user.home");
		File file = new File(userHome + File.separator + LICENSE_KEY_DIR, PRODUCT_NAME + ".key");
		FileInputStream fis = new FileInputStream(file);
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int count = -1;
		while ((count = fis.read(buffer)) != -1) {
			baos.write(buffer, 0, count);
		}
		baos.flush();
		baos.close();
		fis.close();
		return baos.toByteArray();
	}

	/**
	 * License parts.
	 * 
	 * @author Anthony.M
	 *
	 */
	public static final class License implements Serializable {

		private static final long serialVersionUID = 7065227871995198551L;

		private String serverId;
		private Date expireDate;
		private String encodedKey;

		public String getServerId() {
			return serverId;
		}

		public void setServerId(String serverId) {
			this.serverId = serverId;
		}

		public Date getExpireDate() {
			return expireDate;
		}

		public void setExpireDate(Date expireDate) {
			this.expireDate = expireDate;
		}

		public String getEncodedKey() {
			return encodedKey;
		}

		public void setEncodedKey(String encodedKey) {
			this.encodedKey = encodedKey;
		}

	}

	/**
	 * Parses and verifies the license key.
	 * 
	 * @param key license key
	 * @return license parts
	 * @throws AuthenticationException
	 * @throws IOException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeySpecException
	 * @throws InvalidKeyException
	 * @throws SignatureException
	 * @throws ParseException
	 */
	public static final License parseLicenseKey(byte[] key) throws AuthenticationException, IOException,
	        NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException, ParseException {
		if (key.length < HEAD_LENGTH) {
			throw new AuthenticationException("Key length too short");
		}
		int n1 = 0;
		int n2 = 0;
		int n3 = 0;
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		n1 = n1 + (key[1] & 0xFF);
		n1 = n1 + ((key[0] & 0xFF) << Byte.SIZE);
		n2 = n2 + (key[3] & 0xFF);
		n2 = n2 + ((key[2] & 0xFF) << Byte.SIZE);
		n3 = n3 + (key[5] & 0xFF);
		n3 = n3 + ((key[4] & 0xFF) << Byte.SIZE);
		if (key.length - HEAD_LENGTH != n1 + n2 + n3) {
			throw new AuthenticationException("Invalid license key");
		}
		GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(key, HEAD_LENGTH, n1));
		byte[] buffer = new byte[1024];
		int count = -1;
		while ((count = in.read(buffer)) != -1) {
			baos.write(buffer, 0, count);
		}
		baos.flush();
		baos.close();
		in.close();
		byte[] t1 = baos.toByteArray();
		baos = new ByteArrayOutputStream();
		baos.write(key, HEAD_LENGTH + n1, n2);
		baos.flush();
		baos.close();
		byte[] t2 = baos.toByteArray();
		baos = new ByteArrayOutputStream();
		baos.write(key, HEAD_LENGTH + n1 + n2, n3);
		baos.flush();
		baos.close();
		byte[] t3 = baos.toByteArray();
		Signature signature = Signature.getInstance("SHA1WITHRSA");
		KeySpec keySpec = new X509EncodedKeySpec(t3);
		KeyFactory keyFactory = KeyFactory.getInstance("RSA");
		PublicKey publicKey = keyFactory.generatePublic(keySpec);
		signature.initVerify(publicKey);
		signature.update(t1);
		if (!signature.verify(t2)) {
			throw new AuthenticationException("Unrecognized digital signature");
		}
		String regex = "<license><serverId>(.*)</serverId><expireDate>(.*)</expireDate></license>";
		Pattern pattern = Pattern.compile(regex);
		Matcher matcher = pattern.matcher(new String(t1));
		if (!matcher.matches()) {
			throw new AuthenticationException("Illegal license information");
		}
		String serverId = matcher.group(1);
		Date expireDate = new SimpleDateFormat("yyyy-MM-dd").parse(matcher.group(2));
		if (!serverId.equals(getServerId(getMacAddress()))) {
			throw new AuthenticationException("Server id not match");
		}
		if (expireDate.before(new Date())) {
			throw new AuthenticationException("License key expired");
		}
		License license = new License();
		license.setServerId(serverId);
		license.setExpireDate(expireDate);
		license.setEncodedKey(new String(Base64.encodeBase64(key)));
		return license;
	}

	/**
	 * Saves the license key.
	 * 
	 * @param key
	 * @throws IOException
	 */
	public static final void saveLicenseKey(byte[] key) throws IOException {
		String userHome = System.getProperty("user.home");
		File dir = new File(userHome + File.separator + LICENSE_KEY_DIR);
		if (!dir.exists()) {
			dir.mkdirs();
		}
		File file = new File(dir, PRODUCT_NAME + ".key");
		if (!file.exists()) {
			file.createNewFile();
		}
		FileOutputStream fos = new FileOutputStream(file);
		fos.write(key);
		fos.flush();
		fos.close();
	}
	
	private static boolean isLinux() {
	    return System.getProperty("os.name").toUpperCase().startsWith("LINUX");
	}
	
	private static String getLinuxMac() throws IOException {
	   Process p = Runtime.getRuntime().exec("ifconfig eth0"); // Assume the eth0 network interface. 
	   BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
	   String str = null;
	   String hwaddr = "HWADDR";
	   while ((str = br.readLine()) != null) {
	       str = str.toUpperCase();
	       int index = str.indexOf(hwaddr);
	       if (index != -1) {
	           str = str.substring(index + hwaddr.length()).trim().replaceAll(":", "-");
	           break;
	       }
	   }
	   br.close();
	   p.destroy();
	   return str;
	}

}
